using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using Microsoft.CSharp;
using System.Linq;

namespace Kotonoha.Server.UI.Comparators {

    public class ComparatorGenerator {
        private readonly Type type;
        private readonly Dictionary<string, Type> cached = new Dictionary<string, Type>();
        private readonly CSharpCodeProvider codeProvider;
        private readonly CompilerParameters compilerProperties;

        public ComparatorGenerator(Type type) {
            this.type = type;
            codeProvider = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v3.5" } });
            compilerProperties = new CompilerParameters(
                new[] { Assembly.GetExecutingAssembly().Location }) {
                                                                        GenerateExecutable = false,
                                                                        GenerateInMemory = true,
                                                                        CompilerOptions = "/optimize"
                                                                    };
        }

        public Type CreateComparator(string propertyChain) {
            Type t;
            if (!cached.TryGetValue(propertyChain, out t)) {
                string[] names = propertyChain.Trim().Split('.');
                Check(names, propertyChain);
                t = GenerateCodeFor(names);
                cached.Add(propertyChain, t);
            }
            return t;
        }

        private void Check(string[] strings, string propChain) {
            Type t = type;
            foreach (var s in strings) {
                t = GetTypeForProperty(t, s);
            }
            Type compType = Type.GetType(string.Format("System.IComparable`1[{0}]", t.FullName));
            if (!compType.IsAssignableFrom(t)) {
                throw new ComparatorGenerationException(
                    string.Format(
                        "Can't create comparator for property {0} because {1} doesn't impliment IComparable<T>",
                        propChain, t.FullName));
            }
        }

        private static Type GetTypeForProperty(Type t, string s) {
            if (string.IsNullOrEmpty(s)) {
                return t;
            }
            return t.GetProperty(s).PropertyType;
        }

        private Type GenerateCodeFor(string[] names) {
            CodeCompileUnit unit = new CodeCompileUnit();
            unit.ReferencedAssemblies.Add(type.Assembly.Location);
            unit.ReferencedAssemblies.Add("System.dll");
            CodeNamespace ns = new CodeNamespace("Generated");
            ns.Imports.Add(new CodeNamespaceImport(type.Namespace));
            ns.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
            unit.Namespaces.Add(ns);
            var CompName = type.Name + names.Aggregate((s1,s2)=>s1+s2)+"Comparer";
            CodeTypeDeclaration codeType = new CodeTypeDeclaration(CompName);
            codeType.IsClass = true;
            codeType.TypeAttributes = TypeAttributes.Public;
            CodeTypeReference baseType = new CodeTypeReference("IComparer");
            baseType.TypeArguments.Add(type);
            codeType.BaseTypes.Add(baseType);
            ns.Types.Add(codeType);
            CodeMemberMethod method = new CodeMemberMethod();
            method.Name = "Compare";
            method.ReturnType = new CodeTypeReference(typeof(int));
            method.Attributes = MemberAttributes.Public;
            method.ImplementationTypes.Add(baseType);
            string n1 = "x";
            string n2 = "y";
            method.Parameters.Add(new CodeParameterDeclarationExpression(type, n1));
            method.Parameters.Add(new CodeParameterDeclarationExpression(type, n2));
            codeType.Members.Add(method);
            CodeStatementCollection sc = method.Statements;
            MakeBody(sc, names, n1, n2);
            /*StringBuilder b = new StringBuilder();
            StringWriter writer = new StringWriter(b);
            codeProvider.GenerateCodeFromCompileUnit(unit, writer, new CodeGeneratorOptions());
            Debug.WriteLine(writer);*/
            CompilerResults results = codeProvider.CompileAssemblyFromDom(compilerProperties, unit);
            if (results.Errors.HasErrors) {
                throw new ComparatorGenerationException(
                    results.Output.Cast<string>().Aggregate(
                        new StringBuilder(), (sb, s) => sb.AppendLine(s), sb => sb.ToString()));
            }
            return results.CompiledAssembly.GetType("Generated." + CompName);
        }

        private CodeExpression GetReferenceExpression(string varName, string propName) {
            CodeVariableReferenceExpression var = new CodeVariableReferenceExpression(varName);
            if (string.IsNullOrEmpty(propName)) {
                return var;
            }
            return new CodePropertyReferenceExpression(var, propName);
        }

        private void MakeBody(CodeStatementCollection sc, string[] ns, string n1, string n2) {
            List<string> names = new List<string>();
            names.Add("");
            names.AddRange(ns);
            
            //bool b1;
            //bool b2;
            sc.Add(new CodeVariableDeclarationStatement(new CodeTypeReference(typeof (bool)), "b1"));
            sc.Add(new CodeVariableDeclarationStatement(new CodeTypeReference(typeof (bool)), "b2"));
            Type t = type;
            for (int i = 0; i < names.Count; i++) {
                string n = names[i];
                //Word x1 = x;
                //Word y1 = y;
                t = GetTypeForProperty(t, n);
                sc.Add(
                    new CodeVariableDeclarationStatement(
                        new CodeTypeReference(t), 
                        n1 + i,
                        GetReferenceExpression(n1, n)
                        ));
                sc.Add(
                    new CodeVariableDeclarationStatement(
                        new CodeTypeReference(t),
                        n2 + i,
                        GetReferenceExpression(n2, n)
                        ));
                n1 = n1 + i;
                n2 = n2 + i;
//                b1 = x1 == null;
//                b2 = y1 == null;
                sc.Add(
                    new CodeAssignStatement(
                        new CodeVariableReferenceExpression("b1"),
                        new CodeBinaryOperatorExpression(
                            new CodeVariableReferenceExpression(n1),
                            CodeBinaryOperatorType.ValueEquality,
                            new CodePrimitiveExpression(null))));
                sc.Add(
                    new CodeAssignStatement(
                        new CodeVariableReferenceExpression("b2"),
                        new CodeBinaryOperatorExpression(
                            new CodeVariableReferenceExpression(n2),
                            CodeBinaryOperatorType.ValueEquality,
                            new CodePrimitiveExpression(null))));
                /*if (b1 && !b2) {
                    return 1;
                }*/
                sc.Add(
                    new CodeConditionStatement(
                        new CodeBinaryOperatorExpression(
                            new CodeVariableReferenceExpression("b1"),
                            CodeBinaryOperatorType.BooleanAnd, 
                            new CodeSnippetExpression("!b2")),
                        new CodeMethodReturnStatement(
                            new CodePrimitiveExpression(1))
                            ));
                /*if (!b1 && b2) {
                    return -1;
                }*/
                sc.Add(
                    new CodeConditionStatement(
                        new CodeBinaryOperatorExpression(
                            new CodeSnippetExpression("!b1"),
                            CodeBinaryOperatorType.BooleanAnd,
                            new CodeVariableReferenceExpression("b2")),
                        new CodeMethodReturnStatement(
                            new CodePrimitiveExpression(-1))
                            ));

                /*if (b1 && b2) {
                    return 0;
                }*/
                sc.Add(
                    new CodeConditionStatement(
                        new CodeBinaryOperatorExpression(
                            new CodeVariableReferenceExpression("b1"),
                            CodeBinaryOperatorType.BooleanAnd,
                            new CodeVariableReferenceExpression("b2")),
                        new CodeMethodReturnStatement(
                            new CodePrimitiveExpression(0))
                            ));

            }
            CodeMethodReferenceExpression compMethod = new CodeMethodReferenceExpression(
                new CodeVariableReferenceExpression(n1),
                "CompareTo");
            sc.Add(new CodeMethodReturnStatement(new CodeMethodInvokeExpression(compMethod, new CodeVariableReferenceExpression(n2))));
            
        }

        /*public class MyComparator : IComparer<Word> {
            public int Compare(Word x, Word y) {
                bool b1;
                bool b2;
                Word x1 = x;
                Word y1 = y;
                b1 = x == null;
                b2 = y == null;
                if (b1 && !b2) {
                    return 1;
                }
                if (!b1 && b2) {
                    return -1;
                }
                if (b1 && b2) {
                    return 0;
                }
                return 0;
            }
        }*/
    }

    internal class ComparatorGenerationException : Exception {
        public ComparatorGenerationException() {}
        public ComparatorGenerationException(string message) : base(message) {}
        public ComparatorGenerationException(string message, Exception innerException) : base(message, innerException) {}
        protected ComparatorGenerationException(SerializationInfo info, StreamingContext context) : base(info, context) {}
    }
}